package com.basker.pisces.logger;

import java.io.IOException;
import java.nio.charset.Charset;
import java.nio.charset.UnsupportedCharsetException;
import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

import org.apache.commons.lang3.BooleanUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PropertiesLoaderUtils;
import org.springframework.stereotype.Component;
import org.springframework.util.NumberUtils;

import ch.qos.logback.classic.AsyncAppender;
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.LoggerContext;
import ch.qos.logback.classic.encoder.PatternLayoutEncoder;
import ch.qos.logback.classic.filter.LevelFilter;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.Appender;
import ch.qos.logback.core.ConsoleAppender;
import ch.qos.logback.core.FileAppender;
import ch.qos.logback.core.rolling.RollingFileAppender;
import ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy;
import ch.qos.logback.core.spi.FilterReply;
import ch.qos.logback.core.util.FileSize;
import ch.qos.logback.core.util.OptionHelper;

/**
 * 加载属性文件，创建logback的logger对象
 *
 * @author hangwen
 */
@Component
public class LogBackPropertyConfigLoader {

    private static final String LOGGING_NAME_KEY = "logging.name";

    private static final String LOGGING_LEVEL_KEY = "logging.level";
    private static final String LOGGING_ADDITIVE_KEY = "logging.additive";
    private static final String LOGGING_CHARSET_KEY = "logging.charset";

    private static final String LOGGING_CONSOLE_ENABLE_KEY = "logging.console.enable";
    private static final String LOGGING_CONSOLE_PATTERN_KEY = "logging.console.pattern";

    private static final String LOGGING_FILE_ENABLE_KEY = "logging.file.enable";
    private static final String LOGGING_FILE_ASYNC_KEY = "logging.file.async";
    private static final String LOGGING_FILE_ASYNC_QUEUE_SIZE_KEY = "logging.file.async.queue.size";
    private static final String LOGGING_FILE_PATH_KEY = "logging.file.path";
    private static final String LOGGING_FILE_PATTERN_KEY = "logging.file.pattern";
    private static final String LOGGING_FILE_SPLITBYLEVEL_KEY = "logging.file.splitByLevel";
    private static final String LOGGING_FILE_ROLLING_MAXSIZE_KEY = "logging.file.rolling.maxSize";

    private static final String SPRINGBOOT_LOGGING_PATTERN_CONSOLE = "logging.pattern.console";
    private static final String SPRINGBOOT_LOGGING_PATTERN_FILE = "logging.pattern.file";

    private static final String DEFAULT_CONSOLE_PATTERN = "%date{yyyy-MM-dd HH:mm:ss.SSS} %5level ${PID:- } --- [%15.15thread] " +
            "%-50.50logger{49} : %msg%n";
    private static final String DEFAULT_FILE_PATTERN = "%-20(%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread]) %-5level %logger{32} - " +
            "%msg%n";

    private static final String FILE_SUFFIX = ".log";

    @Autowired
    private Environment environment;

    public void load(Resource resouce) throws IOException {
        Properties properties = PropertiesLoaderUtils.loadProperties(resouce);

        String loggerName = properties.getProperty(LOGGING_NAME_KEY);
        if (StringUtils.isEmpty(loggerName)) {
            return;
        }

        LoggerContext loggerContext = (LoggerContext) LoggerFactory.getILoggerFactory();
        Logger logger = loggerContext.getLogger(loggerName);

        boolean additive = BooleanUtils.toBoolean(properties.getProperty(LOGGING_ADDITIVE_KEY, "true"));
        logger.setAdditive(additive);

        // 设置日志等级，默认info
        Level level = getLoggerLevel(properties);
        logger.setLevel(level);

        boolean consoleEnable = BooleanUtils.toBoolean(properties.getProperty(LOGGING_CONSOLE_ENABLE_KEY, "false"));

        if (consoleEnable) {
            // 设置console输出
            ConsoleAppender<ILoggingEvent> consoleAppender = createConsoleAppender(properties, loggerName,
                    loggerContext);
            logger.addAppender(consoleAppender);
        }

        // 设置文件输出
        boolean fileEnable = BooleanUtils.toBoolean(properties.getProperty(LOGGING_FILE_ENABLE_KEY, "true"));
        if (fileEnable) {
            List<Appender<ILoggingEvent>> fileAppenders = createFileAppenders(properties, loggerName, level,
                    loggerContext);
            for (Appender<ILoggingEvent> fileAppender : fileAppenders) {
                logger.addAppender(fileAppender);
            }
        }

    }

    private AsyncAppender createAsyncAppender(Properties properties, RollingFileAppender<ILoggingEvent> fileAppender) {
        AsyncAppender asyncAppender = new AsyncAppender();
        asyncAppender.setContext(fileAppender.getContext());

        // 设置0确保日志不丢失，默认-1，AsyncAppender内部机制会丢弃20%的日志
        asyncAppender.setDiscardingThreshold(0);

        // 设置日志队列大小，默认256
        int queueSize = NumberUtils.parseNumber(properties.getProperty(LOGGING_FILE_ASYNC_QUEUE_SIZE_KEY, "256"),
                Integer.class);
        asyncAppender.setQueueSize(queueSize);

        // 添加fileAppender
        asyncAppender.addAppender(fileAppender);

        asyncAppender.start();

        return asyncAppender;
    }

    private ConsoleAppender<ILoggingEvent> createConsoleAppender(Properties properties, String name,
                                                                 LoggerContext loggerContext) {
        ConsoleAppender<ILoggingEvent> consoleAppender = new ConsoleAppender<>();

        // 设置context和名称
        consoleAppender.setContext(loggerContext);
        consoleAppender.setName(name + "'s console appender");

        String cosolePattern = properties.getProperty(LOGGING_CONSOLE_PATTERN_KEY);
        if (StringUtils.isEmpty(cosolePattern)) {
            cosolePattern = loggerContext.getProperty(SPRINGBOOT_LOGGING_PATTERN_CONSOLE);
            if (StringUtils.isEmpty(cosolePattern)) {
                cosolePattern = DEFAULT_CONSOLE_PATTERN;
            }
        }

        PatternLayoutEncoder encoder = createPatternLayoutEncoder(properties, cosolePattern, loggerContext);
        consoleAppender.setEncoder(encoder);

        // 启动
        consoleAppender.start();

        return consoleAppender;
    }

    private Appender<ILoggingEvent> createFileAppender(Properties properties, String loggerName, Level level,
                                                       LoggerContext loggerContext) {
        RollingFileAppender<ILoggingEvent> fileAppender = new RollingFileAppender<>();
        String levelStr = level != null ? level.toString().toLowerCase() : "";

        // 设置context和名称
        fileAppender.setName(loggerName + "'s " + levelStr + " file appender");
        fileAppender.setContext(loggerContext);

        // 设置输出文件
        String fileName = getFile(properties, loggerName, levelStr);
        String file2Use = OptionHelper.substVars(fileName + FILE_SUFFIX, loggerContext);
        fileAppender.setFile(file2Use);

        // 设置pattern
        String filePattern = properties.getProperty(LOGGING_FILE_PATTERN_KEY);
        if (StringUtils.isEmpty(filePattern)) {
            filePattern = loggerContext.getProperty(SPRINGBOOT_LOGGING_PATTERN_FILE);
            if (StringUtils.isEmpty(filePattern)) {
                filePattern = DEFAULT_FILE_PATTERN;
            }
        }

        PatternLayoutEncoder encoder = createPatternLayoutEncoder(properties, filePattern, loggerContext);
        fileAppender.setEncoder(encoder);

        // 设置level过滤器
        if (level != null) {
            LevelFilter levelFilter = createLevelFilter(level);

            fileAppender.addFilter(levelFilter);
        }

        // 设置文件时间和大小的分片策略
        SizeAndTimeBasedRollingPolicy<ILoggingEvent> policy = createSizeAndTimeBasedRollingPolicy(properties,
                loggerContext, fileName, fileAppender);
        fileAppender.setRollingPolicy(policy);

        // 启动
        fileAppender.start();

        boolean async = BooleanUtils.toBoolean(properties.getProperty(LOGGING_FILE_ASYNC_KEY, "true"));
        if (async) {
            return createAsyncAppender(properties, fileAppender);
        } else {
            return fileAppender;
        }
    }

    private List<Appender<ILoggingEvent>> createFileAppenders(Properties properties, String name, Level loggerLevel,
                                                              LoggerContext loggerContext) {
        boolean splitByLevel = BooleanUtils.toBoolean(properties.getProperty(LOGGING_FILE_SPLITBYLEVEL_KEY, "true"));

        List<Appender<ILoggingEvent>> appenders = new ArrayList<>();

        if (splitByLevel) {
            // 按级别分割文件
            Level[] levels = new Level[]{Level.DEBUG, Level.INFO, Level.WARN, Level.ERROR};

            for (Level level : levels) {
                Appender<ILoggingEvent> fileAppender = createFileAppender(properties, name, level, loggerContext);

                if (fileAppender != null) {
                    appenders.add(fileAppender);
                }
            }
        }

        Appender<ILoggingEvent> fileAppender = createFileAppender(properties, name, null, loggerContext);

        if (fileAppender != null) {
            appenders.add(fileAppender);
        }

        return appenders;
    }

    private LevelFilter createLevelFilter(Level level) {
        LevelFilter levelFilter = new LevelFilter();

        levelFilter.setLevel(level);
        levelFilter.setOnMatch(FilterReply.ACCEPT);
        levelFilter.setOnMismatch(FilterReply.DENY);

        levelFilter.start();

        return levelFilter;
    }

    private PatternLayoutEncoder createPatternLayoutEncoder(Properties properties, String pattern,
                                                            LoggerContext loggerContext) {
        PatternLayoutEncoder encoder = new PatternLayoutEncoder();

        Charset charSet = getCharSet(properties);
        encoder.setCharset(charSet);
        encoder.setPattern(OptionHelper.substVars(pattern, loggerContext));
        encoder.setContext(loggerContext);

        encoder.start();

        return encoder;
    }

    private SizeAndTimeBasedRollingPolicy<ILoggingEvent> createSizeAndTimeBasedRollingPolicy(Properties properties,
                                                                                             LoggerContext loggerContext,
                                                                                             String fileName,
                                                                                             FileAppender<?> fileAppender) {
        SizeAndTimeBasedRollingPolicy<ILoggingEvent> policy = new SizeAndTimeBasedRollingPolicy<>();

        String fileNamePattern = OptionHelper.substVars(fileName + "-%d{yyyy-MM-dd}.%i" + FILE_SUFFIX, loggerContext);
        policy.setFileNamePattern(fileNamePattern);

        String fileSize = properties.getProperty(LOGGING_FILE_ROLLING_MAXSIZE_KEY, "10MB");
        policy.setMaxFileSize(FileSize.valueOf(fileSize));

        policy.setContext(loggerContext);

        policy.setParent(fileAppender);
        policy.start();

        return policy;
    }

    private Charset getCharSet(Properties properties) {
        String charsetName = properties.getProperty(LOGGING_CHARSET_KEY, "UTF-8");
        try {
            return Charset.forName(charsetName);
        } catch (UnsupportedCharsetException e) {
            return Charset.forName("UTF-8");
        }
    }

    private String getFile(Properties properties, String loggerName, String levelStr) {
        // ${user.home}/logs/example/myLog.log
        String file = properties.getProperty(LOGGING_FILE_PATH_KEY);
        if (StringUtils.isEmpty(file)) {
            file = "${log.path}/logs/${spring.application.name}/" + loggerName;
        }

        if (!file.endsWith(FILE_SUFFIX)) {
            file += FILE_SUFFIX;
        }

        // ${log.path}/logs/example/myLog.log
        String fileNameWithoutSuffix = file.substring(0, file.length() - FILE_SUFFIX.length());

        if (!StringUtils.isEmpty(levelStr)) {
            // ${log.path}/logs/example/myLog-info.log
            fileNameWithoutSuffix += ("-" + levelStr);
        }

        return fileNameWithoutSuffix;
    }

    private Level getLoggerLevel(Properties properties) {
        String loggerName = properties.getProperty(LOGGING_NAME_KEY);
        String level = this.getPropertyFromEnvioment(loggerName, LOGGING_LEVEL_KEY);

        if (level == null) {
            level = properties.getProperty(LOGGING_LEVEL_KEY, "INFO");
        }

        return Level.toLevel(level, Level.INFO);
    }

    private String getPropertyFromEnvioment(String loggerName, String key) {
        String compositeKey = key + "." + loggerName;
        return this.environment.getProperty(compositeKey);
    }

}
